package pl.th.permission.plugin.transform;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.codehaus.groovy.runtime.ResourceGroovyMethods;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;

import pl.th.permission.plugin.Utils;
import pl.th.permission.plugin.node.PermissionClassNode;

public class SerialTransformer implements Transformer{

    private final boolean isIncremental;
    private final Collection<TransformInput> transformInputs;
    private final TransformOutputProvider outputProvider;

    public SerialTransformer(TransformInvocation invocation) {
        isIncremental = invocation.isIncremental();
        transformInputs = invocation.getInputs();
        outputProvider = invocation.getOutputProvider();
    }

    @Override
    public void transform() throws Exception{
        if (!isIncremental) {
            outputProvider.deleteAll();
        }
        for (TransformInput transformInput : transformInputs) {
            for (DirectoryInput directoryInput : transformInput.getDirectoryInputs()) {
                if (isIncremental) {
                    handleIncrementalDirInput(directoryInput);
                } else {
                    handleNotIncrementalDirInput(directoryInput);
                }
            }
            for (JarInput jarInput : transformInput.getJarInputs()) {
                if (isIncremental) {
                    handleIncrementalJarInput(jarInput);
                } else {
                    handleCopiedJarInput(jarInput);
                }
            }
        }
    }

    private void handleIncrementalDirInput(DirectoryInput input) throws IOException {
        File srcDir = input.getFile();
        Map<File, Status> changedMap = input.getChangedFiles();
        File destDir = outputProvider.getContentLocation(input.getName(),
                input.getContentTypes(), input.getScopes(),
                Format.DIRECTORY);
        for (Map.Entry<File, Status> entry : changedMap.entrySet()) {
            final File srcFile = entry.getKey();
            final Status status = entry.getValue();
            handleIncrementalDirInput(srcFile, status, srcDir, destDir);
        }
    }

    private void handleIncrementalDirInput(File srcFile, Status status, File srcDir, File destDir) throws IOException{
        if (srcFile.isFile()) {
            handleIncrementalDirInputFile(srcFile, status, srcDir, destDir);
        } else {
            if (status == Status.REMOVED) {
                String destPath = srcFile.getAbsolutePath()
                        .replace(srcDir.getAbsolutePath(), destDir.getAbsolutePath());
                File destFile = new File(destPath);
                if (destFile.exists()) FileUtils.forceDelete(destFile);
            } else {
                File[] list = srcFile.listFiles();
                if (list == null) return;
                if (list.length == 0) {
                    String destPath = srcFile.getAbsolutePath()
                            .replace(srcDir.getAbsolutePath(), destDir.getAbsolutePath());
                    File destFile = new File(destPath);
                    if (!destFile.exists()) Utils.mkdirs(destFile.getAbsolutePath());
                    return;
                }
                for (File file : list) {
                    handleIncrementalDirInput(file, status, srcDir, destDir);
                }
            }
        }
    }

    protected void handleIncrementalDirInputFile(File srcFile, Status status, File srcDir, File destDir) throws IOException{
        String destPath = srcFile.getAbsolutePath()
                .replace(srcDir.getAbsolutePath(), destDir.getAbsolutePath());
        File destFile = new File(destPath);
        if (status == Status.ADDED || status == Status.CHANGED) {
            FileUtils.touch(destFile);
            Utils.mkdirs(destFile.getParent());
            processClassFile(srcFile, destFile);
        } else if (status == Status.REMOVED) {
            if (destFile.exists()) FileUtils.forceDelete(destFile);
        }
    }

    private void processClassFile(File srcFile, File destFile) throws IOException{
        if (excludeClassFile(srcFile)) {
            FileUtils.copyFile(srcFile, destFile);
        } else {
            ClassWriter classWriter = new ClassWriter(0);
            ClassReader classReader = new ClassReader(ResourceGroovyMethods.getBytes(srcFile));
            PermissionClassNode classNode = new PermissionClassNode(classWriter);
            classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
            byte[] code = classWriter.toByteArray();
            saveToFile(destFile, code);
        }
    }

    private static void saveToFile(File dest, byte[] bytes) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(dest, false)) {
            fos.write(bytes);
        }
    }


    private void handleNotIncrementalDirInput(DirectoryInput directoryInput) throws IOException{
        File srcDir = directoryInput.getFile();
        File destDir = outputProvider.getContentLocation(directoryInput.getName(),
                directoryInput.getContentTypes(), directoryInput.getScopes(),
                Format.DIRECTORY);
        recurseEachFile(srcDir, srcDir, destDir);
    }

    private void recurseEachFile(File src, File srcDir, File destDir) throws IOException{
        File[] list = src.listFiles();
        if (list == null) return;
        for (File srcFile : list) {
            if (srcFile.isFile()) {
                handleNotIncrementalDirInputFile(srcFile, srcDir, destDir);
            } else {
                recurseEachFile(srcFile, srcDir, destDir);
            }
        }
    }

    protected void handleNotIncrementalDirInputFile(File srcFile, File srcDir, File destDir) throws IOException{
        String destPath = srcFile.getAbsolutePath()
                .replace(srcDir.getAbsolutePath(), destDir.getAbsolutePath());
        File destFile = new File(destPath);
        Utils.mkdirs(destFile.getParent());
        processClassFile(srcFile, destFile);
    }

    private boolean excludeClassFile(File classFile) {
        final String fileName = classFile.getName();
        return !fileName.endsWith(".class") || (fileName.startsWith("R$") ||
                fileName.equals("R.class") || fileName.equals("BuildConfig.class"));
    }

    private void handleIncrementalJarInput(JarInput input) throws IOException{
        final Status status = input.getStatus();
        if (status == Status.ADDED || status == Status.CHANGED) {
            handleCopiedJarInput(input);
        } else if (status == Status.REMOVED) {
            handleRemovedJarInput(input);
        }
    }

    protected void handleCopiedJarInput(JarInput input) throws IOException{
        final File jarFile = input.getFile();
        if (jarFile.getName().contains("module-info")) return;
        final String jarName = input.getName();
        final  String md5Name = DigestUtils.md5Hex(input.getFile().getAbsolutePath());
        final File dest = outputProvider.getContentLocation(jarName + md5Name,
                input.getContentTypes(), input.getScopes(), Format.JAR);
        FileUtils.copyFile(input.getFile(), dest);
    }

    protected void handleRemovedJarInput(JarInput input) throws IOException{
        final String jarName = input.getName();
        final String md5Name = DigestUtils.md5Hex(input.getFile().getAbsolutePath());
        final File dest = outputProvider.getContentLocation(jarName + md5Name,
                input.getContentTypes(), input.getScopes(), Format.JAR);
        if (dest.exists()) FileUtils.forceDelete(dest);
    }
}
